﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using RayDen.Library.Core;
using RayDen.Library.Core.Primitives;
using RayDen.RayEngine.Core.Types;
using RayDen.RayEngine.Engines.XP;
using RayDen.SmallRT.Components;

namespace RayDen.SmallRT.Integrators
{
    internal class WavelengthClusterPathTracerIntegrator : BaseIntegrator
    {
        public const int MaxDepth = 8;
        public const int MaxSpectralSamples = 8;
        WavelengthCluster black = new WavelengthCluster(SampledSpectrum.sampledLambdaStart, (SampledSpectrum.sampledLambdaEnd - SampledSpectrum.sampledLambdaStart) / ((float)MaxSpectralSamples + 0f), MaxSpectralSamples);


        private Brdf[] Brdfs = new Brdf[]
        {
            new Lambertian(),
            new SpecularReflection(),
            new SpecularRefraction(),
            new Lambertian(),

        };

        protected string stats;
        protected long totalSamples = 0, pass = 0;

        class WlCluster
        {
            public float[] Lambda;
            public float[] Value;

            public WlCluster(int n)
            {
                Lambda = new float[n];
                Value = new float[n];
            }

            public void Sort()
            {
                var c = Enumerable.Range(0, Lambda.Length).Select(i => new Tuple<float, float>(Lambda[i], Value[i])).OrderBy(i => i.Item1).ToArray();
                this.Lambda = c.Select(p => p.Item1).ToArray();
                this.Value = c.Select(p => p.Item2).ToArray();
            }

        }

        public override void InitPass()
        {
            //Threads = 1;
        }

        public float SampleWavelength(float u0)
        {

            return SampledSpectrum.sampledLambdaStart + u0 * (SampledSpectrum.sampledLambdaEnd - SampledSpectrum.sampledLambdaStart);
        }

        public override void RenderPass()
        {
            var sb = film.GetFreeSampleBuffer();
            var sw = Stopwatch.StartNew();
            var samplesPerPass = Scene.w * Scene.h;
            float dLambda = (SampledSpectrum.sampledLambdaEnd - SampledSpectrum.sampledLambdaStart) / ((float)MaxSpectralSamples + 1f);
            for (int y = 0; y < Scene.h; ++y)
            {
                int pj = 0;
                for (int x = 0; x < Scene.w; ++x, ++pj)
                {
                    var cluster = new WavelengthCluster(SampledSpectrum.sampledLambdaStart, dLambda, MaxSpectralSamples);
                    //var c = new WlCluster(MaxSpectralSamples);
                    //for (int l = 0; l < MaxSpectralSamples / SamplesPerCluster; l++)
                    {
                        float[] lambdas = Enumerable.Range(0, MaxSpectralSamples).Select(i => SampleWavelength(rnd.NextFloat())).OrderBy(i=>i).ToArray();
                        RayData cameraRay;
                        Scene.GenerateCameraRay(x, y, out cameraRay);
                        cluster = EvalRadiance(ref cameraRay, cluster, 0);
                        totalSamples += MaxSpectralSamples;
                    }
                    //cluster.Sort();
                    IrregularSPD spd = new IrregularSPD(cluster.Lambdas, cluster.Values, MaxSpectralSamples, dLambda);
                    //RegularSPD spd = new RegularSPD(vals, SampledSpectrum.sampledLambdaStart, SampledSpectrum.sampledLambdaEnd, MaxSpectralSamples);
                    var pixV = spd.ToRgb();
                    sb.SplatSample(x, (Scene.h - y - 1), ref pixV);
                    if (sb.IsFull())
                    {
                        film.SplatSampleBuffer(true, sb);
                        sb = film.GetFreeSampleBuffer();
                    }
                }
            }
            pass++;
            sw.Stop();
            stats = string.Format("MSamples per sec {1:F5}; Total Samples {0}; Samples per pixel {2}; {3}", totalSamples, 0.000001 * (samplesPerPass / sw.Elapsed.TotalSeconds), pass, film.GetStats());
        }

        public override string GetStats()
        {
            return stats;
        }


        private WavelengthCluster EvalRadiance(ref RayData r, WavelengthCluster lambdas, int depth)
        {
            double t = 0; // distance to intersection 
            int id = 0; // id of intersected object 
            if (!Scene.Intersect(ref r, ref t, ref id) || depth > MaxDepth)
            {
                return black;
            }
            var obj = SceneManager.Sph[id];
            var x = r.Point((float)t);
            Normal n = (Normal)(x - obj.p).Normalize();
            var nl = Vector.Dot(ref n, ref r.Dir) < 0 ? n : n * -1;
            RayData newRay = new RayData();
            float[] fs = new float[MaxSpectralSamples];
            depth++;
            for (int i = 0; i < MaxSpectralSamples; i++)
            {
                var lambda = lambdas.Lambdas[i];
                var f = obj.c.Sample(lambda);
                var e = 0f;
                var p = f;
                if (obj.refl == 3)
                {
                    e += f;
                }
                if (depth > MaxDepth)
                {
                    if (rnd.NextFloat() < p)
                    {
                        f = f * (1f / p);
                    }
                    else
                    {
                        fs[i] = e;
                    }
                }

                var brdf = Brdfs[obj.refl];
                Vector dir;
                float fb;
                brdf.Sample(ref r.Dir, ref n, rnd.NextFloat(), rnd.NextFloat(), lambda, out dir, out fb);
                newRay = new RayData(ref x, ref dir);
                fs[i] = e + f * fb;
            }
            lambdas.Add(new WavelengthCluster(lambdas.Lambdas, fs));
            return lambdas.Mul(EvalRadiance(ref newRay, lambdas, depth));
        }


        private float EvalRadiance(ref RayData r, float lambda, int depth)
        {
            double t = 0; // distance to intersection 
            int id = 0; // id of intersected object 
            if (!Scene.Intersect(ref r, ref t, ref id) || depth > MaxDepth)
            {
                return 0f;
            }
            var obj = SceneManager.Sph[id];
            var x = r.Point((float)t);
            Normal n = (Normal)(x - obj.p).Normalize();
            var nl = Vector.Dot(ref n, ref r.Dir) < 0 ? n : n * -1;
            var f = obj.c.Sample(lambda);
            var e = 0f;
            var p = f;
            if (obj.refl == 3)
            {
                e += f;
            }
            if (++depth > MaxDepth)
            {
                if (rnd.NextFloat() < p)
                {
                    f = f * (1f / p);
                }
                else
                {
                    return e;
                }
            }

            var brdf = Brdfs[obj.refl];
            Vector dir;
            float fb;
            brdf.Sample(ref r.Dir, ref n, rnd.NextFloat(), rnd.NextFloat(), lambda, out dir, out fb);
            RayData newRay = new RayData(ref x, ref dir);
            return e + f * fb * EvalRadiance(ref newRay, lambda, depth);
        }
    }
}
